home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Personal Computer World 2009 February
/
PCWFEB09.iso
/
Software
/
Resources
/
Browsers, Managers & Extensions
/
Mozilla Geode 1.5
/
geode-latest.xpi
/
content
/
geode.js
next >
Wrap
Text File
|
2008-10-07
|
27KB
|
730 lines
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is labs.mozilla.com code.
*
* The Initial Developer of the Original Code is Mozilla Corporation.
* Portions created by the Initial Developer are Copyright (C) 2008
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Justin Dolske <dolske@mozilla.com> (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
var GeoLocThingie = {
/* ---------- private memebers ---------- */
get _logService() {
delete this._logService;
return this._logService = Cc["@mozilla.org/consoleservice;1"].
getService(Ci.nsIConsoleService);
},
get _observerService() {
delete this._observerService;
return this._observerService = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
},
get _contentPrefService() {
delete this._contentPrefService;
return this._contentPrefService = Cc["@mozilla.org/content-pref/service;1"].
getService(Ci.nsIContentPrefService);
},
// IO service for string -> nsIURI conversion
get _ioService() {
delete this._ioService;
return this._ioService = Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService);
},
_prefBranch : null, // Preferences service
_debug : false,
_pagePermissions : {},
_gps : null,
/*
* init
*
*/
init : function () {
// Cache references to current |this| in utility objects
this._observer._geode = this;
this._webProgressListener._geode = this;
// Preferences.
this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
getService(Ci.nsIPrefService).getBranch("extensions.geode.");
this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
this._prefBranch.addObserver("", this._observer, false);
// Get current preference values.
this._debug = this._prefBranch.getBoolPref("debug");
// Attach to our location provider
this._gps = new GeolocProvider_loki();
this._gps.startup();
// WebProgressListener for getting notification of new doc loads.
// XXX Ugh. Since we're a chrome overlay, it would be nice to just
// use gBrowser.addProgressListener(). But that isn't sending
// STATE_TRANSFERRING, and the earliest we can get at the page is
// STATE_STOP (which is onload, and is inconviently late).
// We'll use the docloader service instead, but that means we need to
// filter out loads for other windows.
var docsvc = Cc["@mozilla.org/docloaderservice;1"].
getService(Ci.nsIWebProgress);
var listener = this._webProgressListener;
docsvc.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
// Remove progress listener when the window is closed.
window.addEventListener("close", function() { docsvc.removeProgressListener(listener); }, false);
// Open a tab for the first-run page, if needed.
if(this._prefBranch.getBoolPref("firstRun"))
this._observerService.addObserver(this._observer, "sessionstore-windows-restored", true);
},
/*
* log
*
* Internal function for logging debug messages to the Error Console window
*/
log : function (message) {
if (!this._debug)
return;
dump("GeoLoc: " + message + "\n");
this._logService.logStringMessage("GeoLoc: " + message);
},
/* ---------- Utility objects ---------- */
/*
* _observer object
*
* Just used for watching preference changes.
*/
_observer : {
_geode : null,
QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
// nsObserver
observe : function (subject, topic, data) {
if (topic == "nsPref:changed") {
var prefName = data;
this._geode.log("got change to " + prefName + " preference");
if (prefName == "debug") {
this._geode._debug =
this._geode._prefBranch.getBoolPref("debug");
} else if (prefName == "whatever") {
// blah
} else {
this._geode.log("Oops! Pref not handled, change ignored.");
}
} else if (topic == "sessionstore-windows-restored") {
// Open a tab for the first-run page, if needed.
// Ugh. The session restore code fires this notification when
// it *starts* processing the last window. If we add the tab
// now, it will just be overwritten by session restore! The
// notification is fired synchronously, so we can use
// setTimeout so we run once it finishes.
if(this._geode._prefBranch.getBoolPref("firstRun")) {
this._geode.log("Showing firstRun page.");
this._geode._prefBranch.setBoolPref("firstRun", false);
setTimeout('gBrowser.selectedTab = gBrowser.addTab("http://labs.mozilla.com/geode_welcome/");', 0);
}
} else {
this._geode.log("Oops! Unexpected notification: " + topic);
}
}
},
/*
* _webProgressListener object
*
* Internal utility object, implements nsIWebProgressListener interface.
* This is attached to the document loader service, so we get
* notifications about all page loads.
*/
_webProgressListener : {
_geode : null,
QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference]),
onStateChange : function (aWebProgress, aRequest,
aStateFlags, aStatus) {
// STATE_START is too early, doc is still the old page.
// STATE_STOP is inconviently late (it's onload)
if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING))
return;
var domWindow = aWebProgress.DOMWindow;
var chromeWin = domWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow);
if (chromeWin != window)
return;
this._geode.log("onStateChange accepted: req = " +
(aRequest ? aRequest.name : "(null)") +
", flags = 0x" + aStateFlags.toString(16));
this._geode._injectNavigatorGeolocator(domWindow);
},
// stubs for the nsIWebProgressListener interfaces which we don't use.
onProgressChange : function() { },
onLocationChange : function() { },
onStatusChange : function() { },
onSecurityChange : function() { },
},
/*
* getShortHostname
*/
_getShortHostname : function (aDOMLocation) {
var shortHostname = aDOMLocation.host;
if (!shortHostname)
shortHostname = aDOMLocation; // for file:///blah.html
return shortHostname;
},
/*
* _eventListener
*/
_eventListener : function (aEvent) {
this.log("_eventListener got " + aEvent.type + " event for " + aEvent.target.location);
// XXX I think the scoping is right here, but might be worth checking
// for any funkyness when the request comes from an iframe or such.
var win = aEvent.target;
if (!(win instanceof Ci.nsIDOMWindow)) {
this.log("Error: event target wasn't a window.")
return;
}
// If the user already granted approval, don't ask again.
var [havePerms, allowed, fuzzLevel] = this._getPagePermissions(win.location);
if (havePerms) {
if (allowed) {
this.log("Page allowed access.");
this._sendPositionUpdate(win, fuzzLevel)
return;
} else {
this.log("Page not allowed access.");
return;
}
}
this.log("Requesting user permission to reveal location");
var promptName = "geode-request";
var shortHostname = this._getShortHostname(win.location);
var promptText = "The page at " + shortHostname +
" wants to know where you are. Tell them:";
var geode = this; // just for the closure
var buttons = [
{
label: "Exact location",
accessKey: "l",
popup: null,
callback: function(bar) {
fuzzLevel = 0;
geode._setPagePermission(win.location, fuzzLevel, checkbox.checked);
geode._sendPositionUpdate(win, fuzzLevel);
}
},
{
label: "Neighborhood",
accessKey: "h",
popup: null,
callback: function() {
fuzzLevel = 200; // 200m radius
geode._setPagePermission(win.location, fuzzLevel, checkbox.checked);
geode._sendPositionUpdate(win, fuzzLevel);
}
},
{
label: "City",
accessKey: "C",
popup: null,
callback: function() {
fuzzLevel = 10000; //10km radius
geode._setPagePermission(win.location, fuzzLevel, checkbox.checked);
geode._sendPositionUpdate(win, fuzzLevel);
}
},
{
label: "Nothing",
accessKey: "N",
popup: null,
callback: function() {
fuzzLevel = -1;
geode._setPagePermission(win.location, fuzzLevel, checkbox.checked);
geode.log("User denied sending location.");
}
},
];
var notifyBox = this._getNotificationBox(win);
var oldBar = notifyBox.getNotificationWithValue(promptName);
var newBar = notifyBox.appendNotification(
promptText, promptName, null,
notifyBox.PRIORITY_INFO_MEDIUM, buttons);
var checkbox = document.createElementNS(
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
"checkbox");
checkbox.setAttribute("id", "rememberChoice");
checkbox.setAttribute("label", "Always do this without asking");
newBar.appendChild(checkbox);
if (oldBar) {
this.log("(...removing preexisting notification bar)");
notifyBox.removeNotification(oldBar);
}
},
/* ------- Internal methods / callbacks for document integration ------- */
_scriptToInject : null,
/*
* _injectNavigatorGeolocator
*
* Injects window.navigator.geolocation into the specified DOM window.
*/
_injectNavigatorGeolocator: function (aWindow) {
this.log("Injecting to " + aWindow.location);
if (!this._scriptToInject) {
this.log("Reading code for injection...");
this._scriptToInject = this._readFile();
}
var sandbox = new Components.utils.Sandbox(aWindow);
sandbox.__proto__ = aWindow.wrappedJSObject
Components.utils.evalInSandbox(this._scriptToInject, sandbox);
aWindow.addEventListener("x-geode-locationRequest-getPosition", function (e) { GeoLocThingie._eventListener(e); }, false, true);
aWindow.addEventListener("x-geode-locationRequest-watchPosition", function (e) { GeoLocThingie._eventListener(e); }, false, true);
aWindow.addEventListener("x-geode-locationRequest-stopWatchPosition", function (e) { GeoLocThingie._eventListener(e); }, false, true);
},
/*
* _readFile
*
* Read a file from the extension's directory, and return it as a string.
*/
_readFile : function () {
// Get at the directory where the extension is installed.
var MY_ID = "geode@labs.mozilla.com";
var em = Cc["@mozilla.org/extensions/manager;1"].
getService(Ci.nsIExtensionManager);
var file = em.getInstallLocation(MY_ID).getItemFile(MY_ID, "install.rdf").parent;
// Get at $EXTDIR/content/injected.js
file.append("content");
file.append("injected.js");
if (!file.exists()) {
this.log("ERROR: $EXTDIR/content/injected.js doesn't exist!");
return;
}
// Slurp the contents of the file into a string.
var line = { value: "" };
var inputStream, lineStream, hasMore;
inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(Ci.nsIFileInputStream);
inputStream.init(file, 0x01, -1, null); // RD_ONLY
lineStream = inputStream.QueryInterface(Ci.nsILineInputStream);
var bigString = "", hasMore;
do {
hasMore = lineStream.readLine(line);
bigString += line.value;
bigString += "\n";
} while (hasMore);
lineStream.close();
return bigString;
},
/*
* _sendPositionUpdate
*/
_sendPositionUpdate : function (aWindow, aFuzzLevel) {
var geode = this;
// Fuzz the exact location we're given, so it's imprecise to the level
// specified by the user. This is a little tricky, because the
// location is in degrees of lat/long, and the fuzz level is in
// meters. Converting latitude (N/S) to meters is basically linear,
// but converting longtitude (E/W) to meters depends on your latitude
// (eg, 1 degree of long. @ the equator is much larger than 1 degree
// near the poles).
function fuzzLocation(pos, fuzzLevel) {
geode.log("Fuzzing location...");
// The following is based on http://en.wikipedia.org/wiki/Latitude#Degree_length
const m1 = 111132.92;
const m2 = -559.82;
const m3 = 1.175;
const m4 = -0.0023;
const p1 = 111412.84;
const p2 = -93.5;
const p3 = 0.118;
var latRads = pos.latitude * (2.0 * Math.PI / 360.0);
// Calculates the degrees-per-meter at a specific lat/long.
var latDegPerMeter = 1 / (m1 +
(m2 * Math.cos(2 * latRads)) +
(m3 * Math.cos(4 * latRads)) +
(m4 * Math.cos(6 * latRads)));
var longDegPerMeter = 1 / ((p1 * Math.cos(1 * latRads)) +
(p2 * Math.cos(3 * latRads)) +
(p3 * Math.cos(5 * latRads)));
var latFuzz = latDegPerMeter * fuzzLevel;
var longFuzz = longDegPerMeter * fuzzLevel;
geode.log("Fuzzlevel " + fuzzLevel + " --> " + longFuzz + " x " + latFuzz);
// If we're fuzzing, set the accuracy.
// XXX seems like the right thing to do, but maybe we shouldn't
// let the site know we're fuzzing?
if (fuzzLevel && fuzzLevel > pos.accuracy)
pos.accuracy = fuzzLevel;
// Scale by fuzz by a randon number between -1 and 1.
pos.latitude += (2 * Math.random() - 1) * latFuzz;
pos.longtitude += (2 * Math.random() - 1) * longFuzz;
// Overflow clamping / wrapping.
if (pos.latitude < -90)
pos.latitude = -90;
else if (pos.latitude > 90)
pos.latitude = 90;
if (pos.longitude < -180)
pos.longitude += 360;
else if (pos.longitude > 180)
pos.longitude -= 360;
}
function reallySendPositionUpdate(message) {
geode.log("sending position update to " + aWindow.location);
var sandbox = new Components.utils.Sandbox(aWindow);
sandbox.__proto__ = aWindow.wrappedJSObject
scriptToRun = "window.navigator.geolocation._notifyListeners(" +
message.toSource() + ");";
Components.utils.evalInSandbox(scriptToRun, sandbox);
}
function successHandler(position) {
geode.log("Loki provider got position");
// nsIDOMGeoPosition
var loc = {
latitude : position.latitude,
longitude : position.longitude,
altitude : null,
accuracy : null,
altitudeAccuracy : null,
heading : null,
velocity : null,
timestamp : Date.now(),
};
fuzzLocation(loc, aFuzzLevel);
reallySendPositionUpdate(loc);
}
function failureHandler(error) {
var errorString = "Loki provider failed: ";
switch (error) {
case 1:
errorString += "Scanner not found."; break;
case 2:
errorString += "WiFi not available."; break;
case 3:
errorString += "No WiFi in range."; break;
case 4:
errorString += "Unauthorized."; break;
case 5:
errorString += "Invalid Application Key."; break;
case 6:
errorString += "Location cannot be determined."; break;
case 7:
errorString += "Proxy unauthorized."; break;
default:
errorString += "Unknown error."; break;
}
geode.log("Loki provider failed: " + error +
" (" + errorString + ")");
// nsIDOMGeoPositionError
var err = {
code : error,
message : errorString
};
reallySendPositionUpdate(err);
}
// XXX The actual nsIGeolocationProvider API is awkward to use here,
// because we jsut want to fetch the position asynchronously. For now
// we'll just fudge our way with an async API.
// var loc = this._gps.currentPosition;
this._gps.getLocationAsync(successHandler, failureHandler);
},
/*
* _getPagePermissions
*
* Checks to see if a page should be allowed access to the current
* location, without user interaction.
*
* Returns [havePerms, allowed, fuzzLevel]
*
* havePerms -- true if there's an existing permissions decision.
* allowed -- true if the existing decision was to allow the page access
* to the location, false is the decision was to deny access.
* fuzzLevel -- fuzzlevel for page's allowed access
*/
_getPagePermissions : function (aDOMLocation) {
var shortName = this._getShortHostname(aDOMLocation);
this.log("Checking page permissions for " + shortName);
var uri = this._ioService.newURI(aDOMLocation, null, null);
var fuzzLevel = this._contentPrefService.getPref(uri, "labs.geode.fuzzLevel");
if (typeof fuzzLevel != "undefined") {
this.log("Found permanent pref, fuzz is " + fuzzLevel);
return [true, (fuzzLevel >= 0), fuzzLevel];
}
var perms = this._pagePermissions[shortName];
if (!perms)
return [false, undefined, undefined];
var timeout = perms.lastUsed;
// Keep permission grants/denials alive for a short period of time,
// to help avoid the prompts from being annoying when navigating
// location-aware sites. Maybe we should just get rid of this if
// we can save the choice permanently?
if (perms.allowed)
timeout += 1 * 60 * 1000; // 1 minute
else
timeout += 10 * 1000; // 10 seconds
var now = Date.now();
if (now > timeout) {
this.log("Page previously " + (perms.allowed ? "allowed" : "denied") +
" permission, but has timed out");
delete this._pagePermissions[shortName];
return [false, undefined, undefined];
}
perms.lastUsed = now;
this.log("Page is " + (perms.allowed ? "allowed" : "denied") +
" with fuzz level " + perms.fuzzLevel);
return [true, perms.allowed, perms.fuzzLevel];
},
/*
* _setPagePermission
*
* Remembers a user's choice for allowing a page to automatically get the
* position. The choice is always remembered for a short period of time,
* and can optionally be saved permanently.
*
* aFuzzLevel can be -1 to indicate permission was denied.
*/
_setPagePermission : function (aDOMLocation, aFuzzLevel, isPermanent) {
var shortName = this._getShortHostname(aDOMLocation);
this.log("setting permissions for " + shortName + " to " +
(aFuzzLevel >= 0 ? ("fuzz level " + aFuzzLevel) : "denied"));
if (isPermanent) {
this.log("saving permanently...");
var uri = this._ioService.newURI(aDOMLocation, null, null);
this._contentPrefService.setPref(uri, "labs.geode.fuzzLevel", aFuzzLevel);
} else {
this._pagePermissions[shortName] = {
allowed: (aFuzzLevel >= 0),
fuzzLevel: aFuzzLevel,
lastUsed: Date.now()
};
}
},
/*
* _getNotificationBox
*/
_getNotificationBox : function (aWindow) {
try {
// Get topmost window, in case we're in a frame.
var notifyWindow = aWindow.top
// Find the <browser> which contains notifyWindow, by looking
// through all the open windows and all the <browsers> in each.
var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
getService(Ci.nsIWindowMediator);
var enumerator = wm.getEnumerator("navigator:browser");
var tabbrowser = null;
var foundBrowser = null;
while (!foundBrowser && enumerator.hasMoreElements()) {
var win = enumerator.getNext();
tabbrowser = win.getBrowser();
foundBrowser = tabbrowser.getBrowserForDocument(
notifyWindow.document);
}
// Return the notificationBox associated with the browser.
if (foundBrowser)
return tabbrowser.getNotificationBox(foundBrowser)
} catch (e) {
this.log("No notification box available!");
return null;
}
},
};
window.addEventListener("load", function() {GeoLocThingie.init();}, false);
function GeolocProvider_loki() { };
GeolocProvider_loki.prototype = {
_loki : null,
_lokiFailure : function (error) {
dump("LOKI FAILURE: Code " + error + "\n");
},
startup : function () {
this._loki = Cc["@skyhookwireless.com/locationService;1"].
createInstance(Ci.wpsILocationService);
this._loki.onFailure = this._lokiFailure;
this._loki.setKey("mozilla.org");
return;
},
isReady : function () {
return true;
},
watch : function (callback) {
// Loki doesn't seem to have a "tell me when the position changes" API.
throw "not implemented";
},
get currentLocation() {
throw "Loki really wants an async API for this."
},
// XXX this isn't in nsIGeolocationProvider
getLocationAsync : function (successCallback, failureCallback) {
this._loki.onSuccess = successCallback;
this._loki.onFailure = failureCallback;
this._loki.requestLocation(true, this._loki.LIMITED_STREET_ADDRESS_LOOKUP);
},
shutdown : function () {
this._loki = null;
return;
}
}; // end of GeolocProvider_loki implementation